# -*- coding: utf-8 -*-

import errno
import logging
import os
import re
import time
from collections import OrderedDict
from contextlib import closing

import paramiko

logging.basicConfig(level=logging.DEBUG,
                    format='[%(asctime)s] %(levelname)s %(module)s:%(lineno)d %(message)s')
LOG = logging.getLogger(__name__)

HOST = {
    'localhost': {
        'host': 'localhost',
        'username': 'catroll',
        'password': '532084',
    },
    'jump': {
        'host': '10.10.217.214',
        # 'password': 'qVbKuqhZWb4NC94txMYQehUmGDCLpPX1jJdy8ySV0ukcD5ED0c8LGCuK4kMp244I',

        'username': 'huang',
        'password': 'ID7GOVrG-Wr-Kc-F',
    },
    'hk.jump': {
        'host': '23.248.162.92',
        'key_filename': 'jump.sendcloud.io-id_rsa'
    },
    'test-gr5h': {
        'host': '10.8.30.115',
        'key_filename': 'hk.jump.sendcloud.io-id_rsa'
    },
    'in-1': {
        'host': '10.9.13.213',
        'key_filename': 'jump.sendcloud.io-id_rsa',
    },
    'sb2': {
        'host': '10.9.172.171',
        'key_filename': 'jump.sendcloud.io-id_rsa',
    },
}


def mkdir_p(path):
    try:
        os.makedirs(path)
    # Python >2.5 (except OSError, exc: for Python <2.5)
    except OSError as exc:
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise


class SSHConnection(object):
    def __init__(self, host, port=22, username='root', password=None, key_filename=None):
        assert isinstance(port, int), 'port must be integer: %r' % port

        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.transport = None
        self.channels = {}

        self.key_filename = key_filename
        if self.key_filename:
            LOG.info('Found private key: %s', self.key_filename)
            self.key = paramiko.RSAKey.from_private_key(
                open(os.path.expanduser('~/.ssh/%s' % self.key_filename)))
        else:
            self.key = None

        self.created_dirs = []

    def connect(self, via=None):
        LOG.debug('connecting...')
        if via:
            assert isinstance(
                via, SSHConnection), 'via %r is not instance of SSHConnection' % via
            LOG.info('Connecting to %s@%s:%d (via %s)',
                     self.username, self.host, self.port, via.host)
            chan = via.channel((self.host, self.port))
            tran = paramiko.Transport(chan)
        else:
            LOG.info('Connecting to %s@%s:%d',
                     self.username, self.host, self.port)
            tran = paramiko.Transport((self.host, self.port))

        # tran.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        tran.start_client()
        if not tran.is_authenticated() and self.username:
            if self.key:
                LOG.info('Trying login %s:%d as %s (use publickey %s)',
                         self.host, self.port, self.username, self.key_filename)
                tran.auth_publickey(self.username, self.key)
            elif self.password:
                LOG.info('Trying login %s:%d as %s (use password)',
                         self.host, self.port, self.username)
                tran.auth_password(self.username, self.password)

        self.transport = tran
        self.sftp = paramiko.SFTPClient.from_transport(self.transport)

        LOG.info('Is this connection authenticated? %r',
                 self.transport.is_authenticated())
        return self

    def channel(self, host):
        if not self.transport:
            self.connect()

        if host not in self.channels:
            # setup forwarding from 127.0.0.1:<free_random_port> to host
            kind = 'direct-tcpip'
            dest_addr = host
            src_addr = ('127.0.0.1', 0)
            LOG.info('[%s:%s] Open channel: kind %r, src_addr %r => dest_addr %r',
                     self.host, self.port, kind, src_addr, dest_addr)
            chan = self.transport.open_channel(kind, dest_addr, src_addr)
            self.channels[host] = chan
        return self.channels[host]

    def execute(self, command, stdin=None, timeout=-1, buff_size=1024):
        assert isinstance(
            command, str), 'command must be a string, but got %r' % command
        LOG.info('[%s:%s] Execute command: %s', self.host, self.port, command)

        channel = self.transport.open_session()

        out = ''
        err = ''
        pause = 0.1

        # with closing(chan) as channel:
        # channel.set_combine_stderr(True)
        channel.settimeout(timeout)
        channel.exec_command(command)
        if stdin:
            channel.sendall(stdin)
            channel.shutdown_write()

        while not channel.exit_status_ready():
            LOG.debug('wait %s seconds...', pause)
            time.sleep(pause)
            if channel.recv_ready():
                out += channel.recv(buff_size)
            if channel.recv_stderr_ready():
                err += channel.recv_stderr(buff_size)

        # while channel.recv_ready():
        #     LOG.debug('==========')
        #     out += channel.recv(buff_size)
        # while channel.recv_stderr_ready():
        #     LOG.debug('++++++++++')
        #     err += channel.recv_stderr(buff_size)

        LOG.debug('Channel closed? %r', channel.closed)
        while not channel.closed:
            # LOG.debug('...')
            # if channel.recv_ready():
            #     _out = channel.recv(buff_size)
            #     LOG.debug('OUT: %r', _out)
            #     out += _out
            # else:
            #     LOG.debug('not recv ready')
            # if channel.recv_stderr_ready():
            #     _err = channel.recv_stderr(buff_size)
            #     LOG.debug('ERR: %r', _err)
            #     err += _err
            # else:
            #     LOG.debug('not recv stderr ready')
            if channel.recv_ready():
                out += channel.recv(buff_size)
            if channel.recv_stderr_ready():
                err += channel.recv_stderr(buff_size)

        LOG.debug('channel.recv_ready()? %r', channel.recv_ready())
        while channel.recv_ready():
            out += channel.recv(buff_size)
        LOG.debug('channel.recv_stderr_ready()? %r',
                  channel.recv_stderr_ready())
        while channel.recv_stderr_ready():
            err += channel.recv_stderr(buff_size)

        code = channel.recv_exit_status()

        LOG.debug('\n' + ('\n'.join(out.splitlines()[-5:])))
        # LOG.debug('ExitCode: %s', code)
        # LOG.debug('StdOut: %s', out)
        # LOG.debug('StdErr: %s', err)
        return code, out, err

    def upload(self, local_path, remote_path):
        dirname = os.path.dirname(remote_path)
        if dirname not in self.created_dirs:
            self.execute('mkdir -p ' + dirname)
            self.created_dirs.append(dirname)

        try:
            LOG.debug('Upload %s to %s (SIZE: %.2fMB)', local_path,
                      remote_path, os.path.getsize(local_path) / 1024 / 1024)
            self.sftp.put(local_path, remote_path)
        except Exception as error:
            LOG.exception(error)

    def download(self, remote_path, local_path):
        try:
            mkdir_p(os.path.dirname(local_path))
            LOG.debug('Download %s to %s', remote_path, local_path)
            self.sftp.get(remote_path, local_path)
        except Exception as error:
            LOG.exception(error)

    def close(self):
        self.transport.close()


class SSHChains(object):
    def __init__(self):
        self.connections = []
        self.transport = None

    def use(self, connection_index=-1):
        return self.connections[connection_index]

    def append(self, host, port=22, username='root', password=None, key_filename=None):
        ssh = SSHConnection(host, port, username, password, key_filename)
        if not self.connections:
            self.connections.append(ssh.connect())
        else:
            last_connection = self.connections[-1]
            self.connections.append(ssh.connect(last_connection))

    def close(self):
        for client in self.connections:
            client.close()

    def execute(self, *args, **kwargs):
        return self.use().execute(*args, **kwargs)


c = SSHChains()


def build_test_connection():
    c.append(**HOST['jump'])
    LOG.info('-' * 50)
    c.append(**HOST['hk.jump'])
    LOG.info('-' * 50)
    c.append(**HOST['test-gr5h'])
    LOG.info('-' * 50)


def test_conn():
    SSHConnection(**HOST['localhost']).connect()


def test_localhost():
    c.append(**HOST['localhost'])
    c.append(**HOST['localhost'])
    c.execute('uname -a')


def test_jump():
    c.append(**HOST['jump'])
    c.append(**HOST['in-1'])
    c.execute('uname -a')


def test_sftp_upload():
    build_test_connection()
    files_will_transfered = [
        # ('/media/Projects/Company/php/dedecms/reference/DedeCMS-V5.7-UTF8-SP2.tar.gz'), '/tmp/DedeCMS-V5.7-UTF8-SP2.tar.gz'),
        ('/media/Projects/Company/php/dedecms/ppzplugin/ppz.tar', '/var/www/html/ppz.tar'),
    ]
    for local_path, remote_path in files_will_transfered:
        c.use().upload(local_path, remote_path)


def test_sftp_download():
    for _dir in [u'/tmp/PayPay猪/UTF-8/', u'/tmp/PayPay猪/GBK/']:
        if not os.path.exists(_dir):
            LOG.debug('Create Directory: %s', _dir)
            os.makedirs(_dir)
        else:
            LOG.debug('Directory %s exists', _dir)

    build_test_connection()
    base_dir = '/media/Projects/Company/php/dedecms/ppzplugin/PayPay猪/'
    files_will_transfered = [
        ('/var/www/html/data/module/31ff32668b207a129b424bbd0fb58699.xml',
         os.path.join(base_dir, 'UTF-8/31ff32668b207a129b424bbd0fb58699.xml')),
        ('/var/www/html/data/module/f01b7ea1b740d2d1d7d640f91c0438ab.xml',
         os.path.join(base_dir, 'GBK/f01b7ea1b740d2d1d7d640f91c0438ab.xml')),
    ]
    for remote_path, local_path in files_will_transfered:
        try:
            c.use().download(remote_path, local_path)
        except Exception as error:
            LOG.warn(error)


def project_deploy():
    # 必须用绝对路径
    remote_path = '/var/www/html/'
    # local_path = '/media/Projects/Company/php/dedecms/uploads/'
    # local_path = '/media/Projects/Company/php/empire/upload/'
    # local_path = '/home/catroll/Projects/Company/php/DiscuzX/upload/'
    local_path = '/home/catroll/Projects/Company/php/wordpress/'

    title = re.sub('[^a-zA-Z0-9]', '-', local_path) + '.'+time.strftime('%Y%m%d-%H%I%S')

    def mdsum_command(directory, find_type='f', match='', not_match=''):
        return ' '.join([
            'find', directory,
            ('-type %s' % find_type) if find_type else '',

            '-regextype posix-extended' if match or not_match else '',
            ('-regex %s' % match) if match else '',
            ('! -regex "%s"' % not_match) if not_match else '',

            """-print0 | xargs -0 md5sum | awk '{printf "%-50s %s\\n", $2, $1}' | sort"""
        ])

    def check_sum():
        # cmd_md5sum = 'find . -type f -name "*.php" -regextype posix-extended ! -regex "./data/(cache|module)/.+" | xargs md5sum'
        # cmd_md5sum = """find . -type f -name "*.php" -regextype posix-extended ! -regex "./data/(cache|module)/.+" ! -regex "./e/(data/adminlogin|payapi)/.+" | xargs md5sum | awk '{printf "%-50s %s\\n", $2, $1}' | sort"""
        # cmd_md5sum = """find . -type f -regextype posix-extended ! -regex "./(\.git|data|fdg|config|uc_server)/.+" ! -print0 | xargs -0 md5sum | awk '{printf "%-50s %s\\n", $2, $1}' | sort"""

        # ignore_patterns_str = '(%s)' % ('|'.join([
        #     '\.\/(%s)\/.+' % ('|'.join(map(re.escape, [
        #         '.git', '.idea',
        #         'woocommerce',
        #         'wp-admin',
        #         'wp-content/themes',
        #         'wp-content/plugins/woocommerce-gateway-paypayzhu/logs'
        #     ]))),
        # ]))
        # cmd_md5sum = ('find . -type f -regextype posix-extended ! -regex "', ignore_patterns_str,
        #               """" ! -print0 | xargs -0 md5sum | awk '{printf "%-50s %s\\n", $2, $1}' | sort""")
        cmd_md5sum = mdsum_command('wp-content/plugins/woocommerce-gateway-paypayzhu', not_match='.+/logs/.+')
        # locally
        command = 'cd ' + local_path + '; ' + cmd_md5sum
        LOG.info('local comand: %s', command)
        content = os.popen(command).read()
        with open('/tmp/%s.a.txt' % title, 'w') as _file:
            _file.write(content)
        local_sums = OrderedDict((_file, _sum) for _file, _sum in [line.split() for line in content.splitlines()])

        # remotely
        command = 'cd ' + remote_path + '; ' + cmd_md5sum
        LOG.info('remote comand: %s', command)
        code, out, err = c.execute('cd ' + remote_path + '; ' + cmd_md5sum, buff_size=1024000)
        with open('/tmp/%s.b.txt' % title, 'w') as _file:
            _file.write(out)
        remote_sums = OrderedDict((_file, _sum) for _file, _sum in [line.split() for line in out.splitlines()])

        # LOG.debug(local_sums)
        # LOG.debug(remote_sums)
        # remote_sums = {}
        # return

        is_synced = True
        LOG.info('*' * 50)
        LOG.info('')
        for _file in local_sums:
            if _file not in remote_sums:
                is_synced = False
                LOG.info('🐈 [LOCAL] ' + _file)
                continue
            if local_sums[_file] != remote_sums[_file]:
                is_synced = False
                LOG.info('🐍 [DIFF] ' + _file)
                continue
            # LOG.info('[SAME] ' + _file + ' ignore it')
        for _file in remote_sums:
            if _file not in local_sums:
                is_synced = False
                LOG.info('🐦 [REMOTE] ' + _file)
        if is_synced:
            LOG.info(u'㊗️ ㊗️ ㊗️ Perfect!!! ㊗️ ㊗️ ㊗️'.center(44))
        LOG.info('')
        LOG.info('*' * 50)

    def file_sync():
        # SFTP
        # c.use().download('/root/.bashrc', '/tmp/test-gr5h.bashrc')

        # c.use().upload('/media/sf_D_DRIVE/empire01.2018-09-05 16_05_18.sql', '/root/empire.sql')
        # c.use().upload('/media/Projects/Company/php/empire/upload.tgz', '/root/empire.tgz')
        # return

        ######################## download ########################
        files = [
            # 'e/admin/ecmsadmin.php',
            # 'e/config/config.php',
            # 'e/extend/paypayzhu/database.sqlite3',
        ]
        for path in files:
            c.use().download(os.path.join(remote_path, path), os.path.join('/tmp/empire/', path))
        # return

        ######################## upload ##########################
        files = [
            # 'include/payment/wait.html',
            # 'include/payment/ppz.php',
            # 'include/ppzcore.php',
            # 'ppzactions.php',

            # 'e/admin/ecmsadmin.php',
            # 'e/payapi/ShopPay.php',
            # 'e/class/connect.php',

            # 'e/extend/paypayzhu/payapi/payend.php',
            # 'e/extend/paypayzhu/payapi/to_pay.php',
            # 'e/extend/paypayzhu/index.php',
            # 'e/extend/paypayzhu/install.php',
            # 'e/extend/paypayzhu/ppz.html',
            # 'e/extend/paypayzhu/ppz.php',
            # 'e/extend/paypayzhu/ppzcore.php',
            # 'e/extend/paypayzhu/ppzdb.php',
            # 'e/extend/paypayzhu/ppzdbViewer.php',
            # 'e/extend/paypayzhu/ppzlogger.php',
            # 'e/extend/paypayzhu/uninstall.php',
            # 'e/extend/paypayzhu/wait.html',
            # 'e/extend/paypayzhu_clean/',

            # 'discuz.tgz',
            # 'ultrax.2018-10-08 16_07_59.sql',
            # 'ultrax.2018-10-08 18_16_44.sql',
            # 'index.php',

            # 'ppz.php',
            # 'api/trade/ppzcore.php',
            # 'api/trade/ppzdb.php',
            # 'api/trade/ppzlogger.php',
            # 'api/trade/ppzorder.php',
            # 'api/trade/api_ppz.php',
            # 'api/trade/api_ppzwxn.php',
            # 'api/trade/api_ppzzfb.php',
            # 'source/module/forum/forum_trade.php',

            # 'source/plugin/ppz/discuz_plugin_ppz.xml',
            # # 'source/plugin/ppz/discuz_xml.php',
            # 'source/plugin/ppz/help.inc.php',
            # 'source/plugin/ppz/index.htm',
            # 'source/plugin/ppz/install.php',
            # 'source/plugin/ppz/main.inc.php',
            # 'source/plugin/ppz/notify.inc.php',
            # 'source/plugin/ppz/order.inc.php',
            # 'source/plugin/ppz/pay.inc.php',
            # 'source/plugin/ppz/payment.class.php',
            # 'source/plugin/ppz/uninstall.php',
            # 'source/plugin/ppz/util.func.php',
            # 'source/plugin/ppz/template/order.htm',
            # 'source/plugin/ppz/template/mobile.htm',
            # 'source/plugin/ppz/template/pay.htm',
            # 'source/class/helper/helper_notification.php',

            # 'source/plugin/ppz/action.inc.php',
            # 'source/plugin/ppz/ppzcore.php',
            # 'source/plugin/ppz/ppzdb.php',
            # 'source/plugin/ppz/ppzlogger.php',
            # 'source/plugin/ppz/ppzorder.php',

            # 'source/plugin/codepay/',
        ]
        files = [
            # '../wordpress.zip',
            # 'database.sql',

            # 'wp-content/plugins/woocommerce-gateway-paypayzhu/',
            'wp-content/plugins/woocommerce-gateway-paypayzhu/lib/core.php',
            'wp-content/plugins/woocommerce-gateway-paypayzhu/woocommerce-gateway-paypayzhu.php',
        ]
        ignore_patterns = ['.+/ppz\.log.+']
        re_ignore = re.compile('(%s)' % (')|('.join(ignore_patterns)))
        for path in files:
            fullpath = os.path.join(local_path, path)
            move_tasks = []
            if os.path.isdir(fullpath):
                for root, dirs, files in os.walk(fullpath):
                    for _file in files:
                        _fullpath = os.path.join(root, _file)
                        if re_ignore.search(_fullpath):
                            continue
                        relpath = os.path.relpath(_fullpath, local_path)
                        move_tasks.append((_fullpath, os.path.join(remote_path, relpath)))
            else:
                move_tasks.append((fullpath, os.path.join(remote_path, path)))
            for _m_local, _m_remote in move_tasks:
                # print _m_local, _m_remote
                # continue
                c.use().upload(_m_local, _m_remote)

    import sys
    action = sys.argv[1] if len(sys.argv) > 1 else 'all'
    if action in ('check', 'sync', 'all'):
        build_test_connection()
        if action == 'check':
            check_sum()
        elif action == 'sync':
            file_sync()
        else:
            file_sync()
            check_sum()


def sb2():
    remote_path = '/opt/softbounce/'
    local_path = '/tmp/softbounce/'
    c.append(**HOST['jump'])
    LOG.info('-' * 50)
    c.append(**HOST['sb2'])
    LOG.info('-' * 50)
    files = [
        # '2018-09-14-15-07-12_Yi5tdY',
        # '2018-09-14-15-07-08_8S8z_R',
        # '2018-09-14-14-57-29_gZCVlY',
        # '2018-09-14-14-57-28_ODTOh9',
        # '2018-09-14-14-54-00_tYeZqD',
        # '2018-09-14-14-39-54_BUHBWQ',
        # '2018-09-14-14-38-14_9wbewm',
        # '2018-09-14-14-35-51_mUBS9s',
        # '2018-09-14-14-33-33_CdpyMm',
        # '2018-09-14-14-31-34_RjFykp',
        # '2018-09-14-14-25-31_sjrMGk',
        # '2018-09-14-14-21-13_darhFz',
        # '2018-09-14-14-07-34_LvUb_n',
        # '2018-09-14-14-07-24_SgL6n6',
        # '2018-09-14-14-05-46_KLlBIb',
        # '2018-09-14-13-55-20_Rw4Jzm',
        # '2018-09-14-13-45-32_uj_6vk',
        # '2018-09-14-13-25-57_tvnCH1',
        # '2018-09-14-13-19-37_F1QlPo',
        # '2018-09-14-13-06-09_0vCFDg',
        # '2018-09-14-12-36-08_KdRL9n',
        # '2018-09-14-12-18-12_8XwLyL',
        # '2018-09-14-12-00-19_1IvUXh',
        # '2018-09-14-12-00-06_LbFFjW',
        # 'ignore/2018/09/10/1536515069637_M12-183.163.COM',
        # 'ignore/2018/09/14/1536873608110_M12-184.163.COM',
        # 'ignore/2018/09/14/1536920448635_EMAIL-GATEWAY-01.CGBCHINA.COM.CN',
        'ignore/2018/09/14/1536900756425_QQ.COM',
    ]
    for path in files:
        c.use().download(os.path.join(remote_path, path), os.path.join(local_path, path))


if __name__ == '__main__':
    # test_sftp_upload()
    # test_sftp_download()

    # sb2()
    project_deploy()

    c.close()
    LOG.info(' OVER '.center(50, '~'))
